昨天我們已經順利讓 TodoListComponent 可以順利在 AppComponent 裡使用了,接下來為了方便大家練習,我們直接從 TodoMVC 的 Source 借 HTML 與 CSS 來用。
我們先用以下的 HTML 替換掉原本在 todo-list.component.html
裡的 HTML:
<section class="todoapp">
<header class="header">
<h1>todos</h1>
<input
class="new-todo"
placeholder="What needs to be done?"
autofocus
>
</header>
</section>
然後再把一些全系統共用的樣式設定貼到 src/
底下的 style.css
裡:
html,
body {
margin: 0;
padding: 0;
}
button {
margin: 0;
padding: 0;
border: 0;
background: none;
font-size: 100%;
vertical-align: baseline;
font-family: inherit;
font-weight: inherit;
color: inherit;
-webkit-appearance: none;
appearance: none;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
line-height: 1.4em;
background: #f5f5f5;
color: #4d4d4d;
min-width: 230px;
max-width: 550px;
margin: 0 auto;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-weight: 300;
}
:focus {
outline: 0;
}
.hidden {
display: none;
}
再來是 todo-list.component.css
的部份,程式碼太長我就不貼在這裡佔篇幅了,請直接點我下載。
筆者不小心把原檔刪除了,還請直接參考 TodoMVC 的原始碼
完成後應該會看到以下畫面:
到目前為止我們所做的事是先將主要的輸入框做出來,這樣才有個地方讓我們輸入待辦的事項。
有了輸入框之後,接下來就是要將使用者所輸入的待辦事項新增到清單內。我們希望使用者輸入完待辦事項後,直接按下 enter
就可以將其所輸入的待辦事項加入到清單內。
所以我們先在輸入框上綁定一個 keyup.enter
的事件,並指定 addTodo
函式去處理這個事件,且將 $event.target
當做參數傳入:
<input
class="new-todo"
placeholder="What needs to be done?"
autofocus
(keyup.enter)="addTodo($event.target)"
>
其實這一段在原本的程式碼裡是使用 [(ngModel)]
來處理雙向綁定,但我在這裡故意採用另外一種方式來告訴大家,既然是事件綁定,其實就有個 $event
的參數可以使用。然後我們可以從 $event.target
來取得觸發當前事件的元素實體,進而取得這個元素的值。
接著我們再到 todo-list.component.ts
裡,實作這個 addTodo
函式:
/**
* 新增代辦事項
*
* @param {HTMLInputElement} inputRef - 輸入框的元素實體
* @memberof TodoListComponent
*/
addTodo(inputRef: HTMLInputElement): void {
console.log(inputRef.value);
inputRef.value = '';
}
為避免有朋友看不懂上述程式碼,我簡單說明一下:
addTodo
是函式名稱,應該沒有人不知道吧?!
inputRef
指的是我們在 Template 使用 $event.target
取到的當前觸發事件的這個元素實體。
HTMLInputElement
是 inputRef
的資料類型。我習慣會替參數宣告資料類型,也建議大家這麼做。因為 VSCode 會幫你檢查你傳入的參數型別有沒有問題 (如果是從 Template 傳入的倒是檢查不到),而且也會提示你這個參數有什麼屬性跟方法可以使用,可以節省時間且降低因打錯字造成 Bug 風險的,非常貼心!
void
是指這個函式回傳值的資料類型,意思是沒有任何回傳值。
我們來看看效果:
看起來似乎是有達到效果,不過稍微防呆一下應該會更好。不急,我們先讓使用者可以真的把待辦事項顯示在清單上。讓我們先在 todo-list.component.html
裡加上:
<section class="todoapp">
<header class="header">
<h1>todos</h1>
<input
class="new-todo"
placeholder="What needs to be done?"
autofocus
(keyup.enter)="addTodo($event.target)"
>
</header>
<!-- 清單區域開始 -->
<section class="main">
<ul class="todo-list">
<li>
<div class="view">
<input class="toggle" type="checkbox">
<label>這裡要顯示待辦事項</label>
<button class="destroy"></button>
</div>
</li>
</ul>
</section>
<!-- 清單區域結束 -->
</section>
這時候畫面應該會變成:
接下來我想要新增一個 Service ,將之後 CRUD 的部份都交給這個 Service 來處理,Component 只要專心處理畫面的顯示就好。
所以輸入以下指令來新增 TodoListService:
ng generate service todo-list/todo-list
之所以會是 todo-list/todo-list
是因為,我想要讓 Angular CLI 建立好這個 Service 之後,直接放在 todo-list
資料夾裡面:
剛建好的 TodoListService 長這樣:
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class TodoListService {
constructor() { }
}
然後我們來宣告一個私有的變數 list ,準備用來存放我們所有的代辦事項:
private list: string[] = [];
接著我們新增一個能將使用者所輸入的待辦事項存放到 list 裡的函式:
/**
* 新增待辦事項
*
* @param {string} title - 待辦事項的標題
* @memberof TodoListService
*/
add(title: string): void {
// 避免傳入的 title 是無效值或空白字串,稍微判斷一下
if (title || title.trim()) {
this.list.push(title);
}
}
不過因為 list 是私有變數的關係,所以我們需要再新增一個函式來取得存放在 list 裡的資料:
/**
* 取得待辦事項清單
*
* @returns {string[]}
* @memberof TodoListService
*/
getList(): string[] {
return this.list;
}
好的,這樣 TodoListService 就大致有個雛形了!接下來我們到 TodoListComponent 裡來注入這個 TodoListService 。
先將 TodoListService 引入:
import { TodoListService } from './todo-list.service';
接著直接在 constructor
函式裡加入 todoListService
這個參數並將其資料類型宣告為 TodoListService :
constructor(private todoListService: TodoListService) { }
當我們在 constructor
宣告參數的時候, TypeScript 預設會幫我們建立一個同名變數,並把參數指定給那個同名變數。
意思是,上面一行其實做了像是這樣子的事:
class TodoListComponent {
private todoListService: TodoListService;
constructor(private todoListService: TodoListService) {
this.todoListService = todoListService;
}
}
不過因為 TypeScript 其實會幫我們處理,所以就不用寫那麼多了。
接著我們來實際使用 todoListService 新增待辦事項:
/**
* 新增代辦事項
*
* @param {HTMLInputElement} inputRef - 輸入框的元素實體
* @memberof TodoListComponent
*/
addTodo(inputRef: HTMLInputElement): void {
const todo = inputRef.value.trim();
if (todo) {
this.todoListService.add(todo);
inputRef.value = '';
}
}
除了新增,也要讓 TodoListComponent 能夠把待辦事項的清單顯示在畫面上,所以再新增一個取得清單的函式:
/**
* 取得待辦事項清單
*
* @returns {string[]}
* @memberof TodoListComponent
*/
getList(): string[] {
return this.todoListService.getList();
}
然後我們到 todo-list.component.html 裡將資料綁到畫面上:
<section class="main" *ngIf="getList().length">
<ul class="todo-list">
<li *ngFor="let todo of getList()">
<div class="view">
<input class="toggle" type="checkbox">
<label>{{ todo }}</label>
<button class="destroy"></button>
</div>
</li>
</ul>
</section>
因為希望清單裡面有待辦事項時才顯示,所以我們使用 *ngIf
這個結構型的 Directive ,令其在存放清單裡的資料大於 0 時才會將整個 <section></section>
載入。
然後用之前學過的 *ngFor
來把清單裡的所有待辦事項逐筆迴圈出來,並將待辦事項用插值表達式 {{ todo }}
來將資料綁在 <label></label>
裡。
來看看效果吧:
所以目前為止,我們已經完成了待辦事項的新增與清單的顯示。
來看一下目前程式碼吧!
todo-list.component.html
的部份:
<section class="todoapp">
<header class="header">
<h1>todos</h1>
<input
class="new-todo"
placeholder="What needs to be done?"
autofocus
(keyup.enter)="addTodo($event.target)"
>
</header>
<section class="main" *ngIf="getList().length">
<ul class="todo-list">
<li *ngFor="let todo of getList()">
<div class="view">
<input class="toggle" type="checkbox">
<label>{{ todo }}</label>
<button class="destroy"></button>
</div>
</li>
</ul>
</section>
</section>
todo-list.component.ts
的部份:
import { Component, OnInit } from '@angular/core';
// Service
import { TodoListService } from './todo-list.service';
@Component({
selector: 'app-todo-list',
templateUrl: './todo-list.component.html',
styleUrls: ['./todo-list.component.css']
})
export class TodoListComponent implements OnInit {
constructor(private todoListService: TodoListService) { }
ngOnInit() {
}
/**
* 新增代辦事項
*
* @param {HTMLInputElement} inputRef - 輸入框的元素實體
* @memberof TodoListComponent
*/
addTodo(inputRef: HTMLInputElement): void {
const todo = inputRef.value.trim();
if (todo) {
this.todoListService.add(todo);
inputRef.value = '';
}
}
/**
* 取得待辦事項清單
*
* @returns {string[]}
* @memberof TodoListComponent
*/
getList(): string[] {
return this.todoListService.getList();
}
}
todo-list.service.ts
的部份:
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class TodoListService {
private list: string[] = [];
constructor() { }
/**
* 取得待辦事項清單
*
* @returns {string[]}
* @memberof TodoListService
*/
getList(): string[] {
return this.list;
}
/**
* 新增待辦事項
*
* @param {string} title - 待辦事項的標題
* @memberof TodoListService
*/
add(title: string): void {
// 避免傳入的 title 是無效值或空白字串,稍微判斷一下
if (title || title.trim()) {
this.list.push(title);
}
}
}
那今天就到這邊,想一下、吸收一下,明天再來完成剩下的部份!
明天見!
想問一下 todo-list.component.html 部份
當輸入 ng serve
的時候
會出現錯誤
<section class="todoapp">
<header class="header">
<h1>todos</h1>
<input class="new-todo" placeholder="What needs to be done?" autofocus (keyup.enter)="addTodo($event.target)">
</header>
<section class="main" *ngIf="getList().length">
<ul class="todo-list">
<li *ngFor="let todo of getList()">
<div class="view">
<input class="toggle" type="checkbox">
<label>{{ todo }}</label>
<button class="destroy"></button>
</div>
</li>
</ul>
</section>
</section>
Error: src/app/todo-list/todo-list.component.html:5:103 - error TS2345: Argument of type 'EventTarget | null' is not assignable to parameter of type 'HTMLInputElement'.
Type 'null' is not assignable to type 'HTMLInputElement'.
另也附上 todo-list-component.ts
import { Component, OnInit } from '@angular/core';
import { TodoListService } from './todo-list.service';
@Component({
selector: 'app-todo-list',
templateUrl: './todo-list.component.html',
styleUrls: ['./todo-list.component.css']
})
export class TodoListComponent implements OnInit {
constructor(private todoListService: TodoListService) { }
ngOnInit(): void {
}
addTodo(inputRef: HTMLInputElement): void {
const todo = inputRef.value.trim();
if (todo) {
this.todoListService.add(todo);
inputRef.value = '';
}
}
getList(): string[] {
return this.todoListService.getList();
}
}
最後在 todo-list.component.ts 的addTodo函式改成以下就正常了
可能是Angular版本關係吧? 相隔了2-3年..
addTodo(inputRef: any): void {
const todo = inputRef.value.trim();
if (todo) {
this.todoListService.add(todo);
inputRef.value = '';
}
}
Hi elvisyip,
非常不建議你把 inputRef
的型別改成 any
噢,
從錯誤訊息中可以知道要怎麼改:
Error: src/app/todo-list/todo-list.component.html:5:103 - error TS2345: Argument of type 'EventTarget | null' is not assignable to parameter of type 'HTMLInputElement'. Type 'null' is not assignable to type 'HTMLInputElement'.
這段錯誤訊息的意思是, $event.targe
的型別是 EventTarget | null
,所以它無法指定給我們定義型別為 HTMLInputElement
的參數 inputRef
,尤其是 null
的部份。
因此我建議可以改成這樣會比較好:
addTodo(inputRef: HTMLInputElement | null): void {
if (inputRef) {
return;
}
const todo = inputRef.value.trim();
if (todo) {
this.todoListService.add(todo);
inputRef.value = '';
}
}
你可以試試看 :)
addTodo(inputRef: HTMLInputElement | null): void {
if(inputRef){
return;
}
const todo = inputRef.value.trim();
if (todo) {
this.todoListService.add(todo);
inputRef.value = '';
}
}
不過導致了另1個問題出現
Object is possibly 'null'.const todo = inputRef.value.trim();
inputRef.value = '';
以上2句的 inputRef 都有同1個錯誤
Hi elvisyip,
抱歉,我給你的程式碼有個判斷式寫錯
是 if(!inputRef)
才對,不是 if(inputRef)
意即當 inputRef
是 null
or undefined
時,就不會再繼續執行。
Leo 大您好:
我依照文中方法撰寫 html,但是在 $event.target
處會出現如下錯誤:Type 'null' is not assignable to type 'HTMLInputElement'.
看起來跟樓上大大的錯誤一樣,所以我根據您給他的回覆,把 component 的 addTodo 改成 addTodo(todoThing: HTMLInputElement | null)
,結果出現新的錯誤如下:Type 'EventTarget' is missing the following properties from type 'HTMLInputElement': accept, align, alt, autocomplete, and 284 more
可以請問這要怎麼解決嗎? 謝謝!
以下附上 code 供參考
<section class="todoapp">
<header class="header">
<h1>todos</h1>
<input
class="new-todo"
placeholder="What needs to be done?"
autofocus
(keyup.enter)="addTodo($event.target)">
</header>
</section>
import { Component, OnInit } from '@angular/core';
import { ListContent } from './list-content.model';
@Component({
selector: 'app-todo-list',
templateUrl: './todo-list.component.html',
styleUrls: ['./todo-list.component.css']
})
export class TodoListComponent implements OnInit {
constructor() { }
ngOnInit(): void {
}
addTodo(todoThing: any): void {
if(!todoThing){
return;
}
const todo = todoThing.value.trim();
this.todoListService.addList(todo);
todoThing.value = '';
}
}
Hi joanne_muyun,
我猜想這是因為現在新版本的 Angular 預設已經是使用 strict mode 的 TypeScript ,所以你才會在這邊被擋下來,我建議你在 template 裡改成 addTodo($event)
,然後 ts 應該是 addTodo(event: KeyboardEvent): void { ... }
,型別的部分你在 function 裡處理即可。
Leo 大您好:
感謝您的回覆!根據您的回覆,我把 template 那邊改成 addTodo($event)
,component 那邊則參考官方文件改成以下:
addTodo(event: KeyboardEvent): void {
const todoThing = (event.target as HTMLInputElement);
// 主要只改了上面兩行
if(!todoThing){
return;
}
const todo = todoThing.value.trim();
this.todoListService.addList(todo);
todoThing.value = '';
}
結果出現了對應 template 中 $event
的報錯:Type 'Event' is missing the following properties from type 'KeyboardEvent': altKey, char, charCode, code, and 16 more.
不知道是不是我型別處理的方式不對呢?
感謝您!
我試了修改如下便可以正常執行
<input
class="new-todo"
placeholder="What needs to be done?"
autofocus
(keyup)="addTodo($event)"
>
addTodo(event: KeyboardEvent): void
{
const todoThing = event.target as HTMLInputElement;
if (!todoThing)
{
return;
}
if (event.key === 'Enter')
{
const todo = todoThing.value.trim();
this.todoListService.add(todo);
todoThing.value='';
}
}
Hi wisely_7club,
非常感謝您幫忙回覆
拯救了我的程式碼
Leo大您好,
想請問為什麼list不直接宣告為public而是宣告為private再透過getList()方法取得值呢?
如果宣告為public好像就可以在component.ts中取得list的值了
是為了保護list的值不被service.ts檔案以外的地方改變嗎?
麻煩您解答了,謝謝!
getList():string[]{
return this.todoListService.list;
}
Hi evafly0508,
沒錯唷!雖然這樣的防止效果僅限於開發期間 XD
你也可以照你的想法做沒問題的,每個人都有自己的設計,只要在大方向上沒有錯誤,又或者是說不會難以維護即可。
無法下載了!!!>>直接點我下載。
Hi chun8106,
抱歉我不小心把原檔刪除了,我補上了參考連結,再麻煩你參考該連結^^"
leo大大您好,我還是找不到todo-list.component.css應該要複製哪一段....(前端新手路過QQ)
我好像也是找到這段,但想請問,是要貼在外層style.css底下 還是該component底下的.css呢?